home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / xulrunner / python / feed.py < prev    next >
Encoding:
Python Source  |  2007-11-12  |  83.5 KB  |  2,323 lines

  1. # Miro - an RSS based video player application
  2. # Copyright (C) 2005-2007 Participatory Culture Foundation
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
  17.  
  18. # FIXME import * is really bad practice..  At the very least, lest keep it at
  19. # the top, so it cant overwrite other symbols.
  20. from item import *
  21.  
  22. from HTMLParser import HTMLParser,HTMLParseError
  23. from cStringIO import StringIO
  24. from copy import copy
  25. from datetime import datetime, timedelta
  26. from gtcache import gettext as _
  27. from inspect import isfunction
  28. from new import instancemethod
  29. from urlparse import urlparse, urljoin
  30. from xhtmltools import unescape,xhtmlify,fixXMLHeader, fixHTMLHeader, urlencode, urldecode
  31. import os
  32. import string
  33. import re
  34. import traceback
  35. import xml
  36.  
  37. from database import defaultDatabase, DatabaseConstraintError
  38. from httpclient import grabURL, NetworkError
  39. from iconcache import iconCacheUpdater, IconCache
  40. from templatehelper import quoteattr, escape, toUni
  41. from string import Template
  42. import app
  43. import config
  44. import dialogs
  45. import eventloop
  46. import folder
  47. import menu
  48. import prefs
  49. import resources
  50. import downloader
  51. from util import returnsUnicode, unicodify, chatter, checkU, checkF, quoteUnicodeURL
  52. from fileutil import miro_listdir
  53. from platformutils import filenameToUnicode, makeURLSafe, unmakeURLSafe, osFilenameToFilenameType, FilenameType
  54. import filetypes
  55. import views
  56. import indexes
  57. import searchengines
  58. import sorts
  59. import logging
  60. import shutil
  61. from clock import clock
  62.  
  63. whitespacePattern = re.compile(r"^[ \t\r\n]*$")
  64.  
  65. @returnsUnicode
  66. def defaultFeedIconURL():
  67.     return resources.url(u"images/feedicon.png")
  68.  
  69. @returnsUnicode
  70. def defaultFeedIconURLTablist():
  71.     return resources.url(u"images/feedicon-tablist.png")
  72.  
  73. # Notes on character set encoding of feeds:
  74. #
  75. # The parsing libraries built into Python mostly use byte strings
  76. # instead of unicode strings.  However, sometimes they get "smart" and
  77. # try to convert the byte stream to a unicode stream automatically.
  78. #
  79. # What does what when isn't clearly documented
  80. #
  81. # We use the function toUni() to fix those smart conversions
  82. #
  83. # If you run into Unicode crashes, adding that function in the
  84. # appropriate place should fix it.
  85.  
  86. # Universal Feed Parser http://feedparser.org/
  87. # Licensed under Python license
  88. import feedparser
  89.  
  90. # Pass in a connection to the frontend
  91. def setDelegate(newDelegate):
  92.     global delegate
  93.     delegate = newDelegate
  94.  
  95. # Pass in a feed sorting function 
  96. def setSortFunc(newFunc):
  97.     global sortFunc
  98.     sortFunc = newFunc
  99.  
  100. #
  101. # Adds a new feed using USM
  102. def addFeedFromFile(file):
  103.     checkF(file)
  104.     d = feedparser.parse(file)
  105.     if d.feed.has_key('links'):
  106.         for link in d.feed['links']:
  107.             if link['rel'] == 'start' or link['rel'] == 'self':
  108.                 Feed(link['href'])
  109.                 return
  110.     if d.feed.has_key('link'):
  111.         addFeedFromWebPage(d.feed.link)
  112.  
  113. #
  114. # Adds a new feed based on a link tag in a web page
  115. def addFeedFromWebPage(url):
  116.     checkU(url)
  117.     def callback(info):
  118.         url = HTMLFeedURLParser().getLink(info['updated-url'],info['body'])
  119.         if url:
  120.             Feed(url)
  121.     def errback(error):
  122.         logging.warning ("unhandled error in addFeedFromWebPage: %s", error)
  123.     grabURL(url, callback, errback)
  124.  
  125. # URL validitation and normalization
  126. def validateFeedURL(url):
  127.     checkU(url)
  128.     for c in url.encode('utf8'):
  129.         if ord(c) > 127:
  130.             return False
  131.     if re.match(r"^(http|https)://[^/ ]+/[^ ]*$", url) is not None:
  132.         return True
  133.     if re.match(r"^file://.", url) is not None:
  134.         return True
  135.     match = re.match(r"^dtv:searchTerm:(.*)\?(.*)$", url)
  136.     if match is not None and validateFeedURL(urldecode(match.group(1))):
  137.         return True
  138.     return False
  139.  
  140. def normalizeFeedURL(url):
  141.     checkU(url)
  142.     # Valid URL are returned as-is
  143.     if validateFeedURL(url):
  144.         return url
  145.  
  146.     searchTerm = None
  147.     m = re.match(r"^dtv:searchTerm:(.*)\?([^?]+)$", url)
  148.     if m is not None:
  149.         searchTerm = urldecode(m.group(2))
  150.         url = urldecode(m.group(1))
  151.  
  152.     originalURL = url
  153.     url = url.strip()
  154.     
  155.     # Check valid schemes with invalid separator
  156.     match = re.match(r"^(http|https):/*(.*)$", url)
  157.     if match is not None:
  158.         url = "%s://%s" % match.group(1,2)
  159.  
  160.     # Replace invalid schemes by http
  161.     match = re.match(r"^(([A-Za-z]*):/*)*(.*)$", url)
  162.     if match and match.group(2) in ['feed', 'podcast', 'fireant', None]:
  163.         url = "http://%s" % match.group(3)
  164.     elif match and match.group(1) == 'feeds':
  165.         url = "https://%s" % match.group(3)
  166.  
  167.     # Make sure there is a leading / character in the path
  168.     match = re.match(r"^(http|https)://[^/]*$", url)
  169.     if match is not None:
  170.         url = url + "/"
  171.  
  172.     if searchTerm is not None:
  173.         url = "dtv:searchTerm:%s?%s" % (urlencode(url), urlencode(searchTerm))
  174.     else:
  175.         url = quoteUnicodeURL(url)
  176.  
  177.     if not validateFeedURL(url):
  178.         logging.info ("unable to normalize URL %s", originalURL)
  179.         return originalURL
  180.     else:
  181.         return url
  182.  
  183.  
  184. ##
  185. # Handle configuration changes so we can update feed update frequencies
  186.  
  187. def configDidChange(key, value):
  188.     if key is prefs.CHECK_CHANNELS_EVERY_X_MN.key:
  189.         for feed in views.feeds:
  190.             updateFreq = 0
  191.             try:
  192.                 updateFreq = feed.parsed["feed"]["ttl"]
  193.             except:
  194.                 pass
  195.             feed.setUpdateFrequency(updateFreq)
  196.  
  197. config.addChangeCallback(configDidChange)
  198.  
  199. ##
  200. # Actual implementation of a basic feed.
  201. class FeedImpl:
  202.     def __init__(self, url, ufeed, title = None, visible = True):
  203.         checkU(url)
  204.         if title:
  205.             checkU(title)
  206.         self.url = url
  207.         self.ufeed = ufeed
  208.         self.calc_item_list()
  209.         if title == None:
  210.             self.title = url
  211.         else:
  212.             self.title = title
  213.         self.created = datetime.now()
  214.         self.visible = visible
  215.         self.updating = False
  216.         self.lastViewed = datetime.min
  217.         self.thumbURL = defaultFeedIconURL()
  218.         self.initialUpdate = True
  219.         self.updateFreq = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)*60
  220.  
  221.     def calc_item_list(self):
  222.         self.items = views.toplevelItems.filterWithIndex(indexes.itemsByFeed, self.ufeed.id)
  223.         self.availableItems = self.items.filter(lambda x: x.getState() == 'new')
  224.         self.unwatchedItems = self.items.filter(lambda x: x.getState() == 'newly-downloaded')
  225.         self.availableItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  226.         self.availableItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  227.         self.unwatchedItems.addAddCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  228.         self.unwatchedItems.addRemoveCallback(lambda x,y:self.ufeed.signalChange(needsSignalFolder = True))
  229.         
  230.     def signalChange(self):
  231.         self.ufeed.signalChange()
  232.  
  233.     @returnsUnicode
  234.     def getBaseHref(self):
  235.         """Get a URL to use in the <base> tag for this channel.  This is used
  236.         for relative links in this channel's items.
  237.         """
  238.         return escape(self.url)
  239.  
  240.     # Sets the update frequency (in minutes). 
  241.     # - A frequency of -1 means that auto-update is disabled.
  242.     def setUpdateFrequency(self, frequency):
  243.         try:
  244.             frequency = int(frequency)
  245.         except ValueError:
  246.             frequency = -1
  247.  
  248.         if frequency < 0:
  249.             self.cancelUpdateEvents()
  250.             self.updateFreq = -1
  251.         else:
  252.             newFreq = max(config.get(prefs.CHECK_CHANNELS_EVERY_X_MN),
  253.                           frequency)*60
  254.             if newFreq != self.updateFreq:
  255.                 self.updateFreq = newFreq
  256.                 self.scheduleUpdateEvents(-1)
  257.         self.ufeed.signalChange()
  258.  
  259.     def scheduleUpdateEvents(self, firstTriggerDelay):
  260.         self.cancelUpdateEvents()
  261.         if firstTriggerDelay >= 0:
  262.             self.scheduler = eventloop.addTimeout(firstTriggerDelay, self.update, "Feed update (%s)" % self.getTitle())
  263.         else:
  264.             if self.updateFreq > 0:
  265.                 self.scheduler = eventloop.addTimeout(self.updateFreq, self.update, "Feed update (%s)" % self.getTitle())
  266.  
  267.     def cancelUpdateEvents(self):
  268.         if hasattr(self, 'scheduler') and self.scheduler is not None:
  269.             self.scheduler.cancel()
  270.             self.scheduler = None
  271.  
  272.     # Subclasses should override this
  273.     def update(self):
  274.         self.scheduleUpdateEvents(-1)
  275.  
  276.     # Returns true iff this feed has been looked at
  277.     def getViewed(self):
  278.         return self.lastViewed != datetime.min
  279.  
  280.     # Returns the ID of the actual feed, never that of the UniversalFeed wrapper
  281.     def getFeedID(self):
  282.         return self.getID()
  283.  
  284.     def getID(self):
  285.         try:
  286.             return self.ufeed.getID()
  287.         except:
  288.             logging.info ("%s has no ufeed", self)
  289.  
  290.     # Returns string with number of unwatched videos in feed
  291.     def numUnwatched(self):
  292.         return len(self.unwatchedItems)
  293.  
  294.     # Returns string with number of available videos in feed
  295.     def numAvailable(self):
  296.         return len(self.availableItems)
  297.  
  298.     # Returns true iff both unwatched and available numbers should be shown
  299.     def showBothUAndA(self):
  300.         return self.showU() and self.showA()
  301.  
  302.     # Returns true iff unwatched should be shown 
  303.     def showU(self):
  304.         return len(self.unwatchedItems) > 0
  305.  
  306.     # Returns true iff available should be shown
  307.     def showA(self):
  308.         return len(self.availableItems) > 0 and not self.isAutoDownloadable()
  309.  
  310.     ##
  311.     # Sets the last time the feed was viewed to now
  312.     def markAsViewed(self):
  313.         self.lastViewed = datetime.now() 
  314.         for item in self.items:
  315.             if item.getState() == "new":
  316.                 item.signalChange(needsSave=False)
  317.  
  318.         self.ufeed.signalChange()
  319.  
  320.     ##
  321.     # Returns true iff the feed is loading. Only makes sense in the
  322.     # context of UniversalFeeds
  323.     def isLoading(self):
  324.         return False
  325.  
  326.     ##
  327.     # Returns true iff this feed has a library
  328.     def hasLibrary(self):
  329.         return False
  330.  
  331.     def startManualDownload(self):
  332.         next = None
  333.         for item in self.items:
  334.             if item.isPendingManualDownload():
  335.                 if next is None:
  336.                     next = item
  337.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  338.                     next = item
  339.         if next is not None:
  340.             next.download(autodl = False)
  341.  
  342.     def startAutoDownload(self):
  343.         next = None
  344.         for item in self.items:
  345.             if item.isEligibleForAutoDownload():
  346.                 if next is None:
  347.                     next = item
  348.                 elif item.getPubDateParsed() > next.getPubDateParsed():
  349.                     next = item
  350.         if next is not None:
  351.             next.download(autodl = True)
  352.  
  353.     ##
  354.     # Returns marks expired items as expired
  355.     def expireItems(self):
  356.         for item in self.items:
  357.             expireTime = item.getExpirationTime()
  358.             if (item.getState() == 'expiring' and expireTime is not None and 
  359.                     expireTime < datetime.now()):
  360.                 item.executeExpire()
  361.  
  362.     ##
  363.     # Returns true iff feed should be visible
  364.     def isVisible(self):
  365.         self.ufeed.confirmDBThread()
  366.         return self.visible
  367.  
  368.     def signalItems (self):
  369.         for item in self.items:
  370.             item.signalChange(needsSave=False)
  371.  
  372.     ##
  373.     # Return the 'system' expiration delay, in days (can be < 1.0)
  374.     def getDefaultExpiration(self):
  375.         return float(config.get(prefs.EXPIRE_AFTER_X_DAYS))
  376.  
  377.     ##
  378.     # Returns the 'system' expiration delay as a formatted string
  379.     @returnsUnicode
  380.     def getFormattedDefaultExpiration(self):
  381.         expiration = self.getDefaultExpiration()
  382.         formattedExpiration = u''
  383.         if expiration < 0:
  384.             formattedExpiration = _('never')
  385.         elif expiration < 1.0:
  386.             formattedExpiration = _('%d hours') % int(expiration * 24.0)
  387.         elif expiration == 1:
  388.             formattedExpiration = _('%d day') % int(expiration)
  389.         elif expiration > 1 and expiration < 30:
  390.             formattedExpiration = _('%d days') % int(expiration)
  391.         elif expiration >= 30:
  392.             formattedExpiration = _('%d months') % int(expiration / 30)
  393.         return formattedExpiration
  394.  
  395.     ##
  396.     # Returns "feed," "system," or "never"
  397.     @returnsUnicode
  398.     def getExpirationType(self):
  399.         self.ufeed.confirmDBThread()
  400.         return self.ufeed.expire
  401.  
  402.     ##
  403.     # Returns"unlimited" or the maximum number of items this feed can fall behind
  404.     def getMaxFallBehind(self):
  405.         self.ufeed.confirmDBThread()
  406.         if self.ufeed.fallBehind < 0:
  407.             return u"unlimited"
  408.         else:
  409.             return self.ufeed.fallBehind
  410.  
  411.     ##
  412.     # Returns "unlimited" or the maximum number of items this feed wants
  413.     def getMaxNew(self):
  414.         self.ufeed.confirmDBThread()
  415.         if self.ufeed.maxNew < 0:
  416.             return u"unlimited"
  417.         else:
  418.             return self.ufeed.maxNew
  419.  
  420.     ##
  421.     # Returns the total absolute expiration time in hours.
  422.     # WARNING: 'system' and 'never' expiration types return 0
  423.     def getExpirationTime(self):
  424.         delta = None
  425.         self.ufeed.confirmDBThread()
  426.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  427.         if (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  428.             (self.ufeed.expire == 'system' and expireAfterSetting <= 0)):
  429.             return 0
  430.         else:
  431.             return (self.ufeed.expireTime.days * 24 + 
  432.                     self.ufeed.expireTime.seconds / 3600)
  433.  
  434.     ##
  435.     # Returns the number of days until a video expires
  436.     def getExpireDays(self):
  437.         ret = 0
  438.         self.ufeed.confirmDBThread()
  439.         try:
  440.             return self.ufeed.expireTime.days
  441.         except:
  442.             return timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).days
  443.  
  444.     ##
  445.     # Returns the number of hours until a video expires
  446.     def getExpireHours(self):
  447.         ret = 0
  448.         self.ufeed.confirmDBThread()
  449.         try:
  450.             return int(self.ufeed.expireTime.seconds/3600)
  451.         except:
  452.             return int(timedelta(days=config.get(prefs.EXPIRE_AFTER_X_DAYS)).seconds/3600)
  453.  
  454.     def getExpires (self):
  455.         expireAfterSetting = config.get(prefs.EXPIRE_AFTER_X_DAYS)
  456.         return (self.ufeed.expireTime is None or self.ufeed.expire == 'never' or 
  457.                 (self.ufeed.expire == 'system' and expireAfterSetting <= 0))
  458.  
  459.     ##
  460.     # Returns true iff item is autodownloadable
  461.     def isAutoDownloadable(self):
  462.         self.ufeed.confirmDBThread()
  463.         return self.ufeed.autoDownloadable
  464.  
  465.     def autoDownloadStatus(self):
  466.         status = self.isAutoDownloadable()
  467.         if status:
  468.             return u"ON"
  469.         else:
  470.             return u"OFF"
  471.  
  472.     ##
  473.     # Returns the title of the feed
  474.     @returnsUnicode
  475.     def getTitle(self):
  476.         try:
  477.             title = self.title
  478.             if whitespacePattern.match(title):
  479.                 title = self.url
  480.             return title
  481.         except:
  482.             return u""
  483.  
  484.     ##
  485.     # Returns the URL of the feed
  486.     @returnsUnicode
  487.     def getURL(self):
  488.         try:
  489.             if self.ufeed.searchTerm is None:
  490.                 return self.url
  491.             else:
  492.                 return u"dtv:searchTerm:%s?%s" % (urlencode(self.url), urlencode(self.ufeed.searchTerm))
  493.         except:
  494.             return u""
  495.  
  496.     ##
  497.     # Returns the URL of the feed
  498.     @returnsUnicode
  499.     def getBaseURL(self):
  500.         try:
  501.             return self.url
  502.         except:
  503.             return u""
  504.  
  505.     ##
  506.     # Returns the description of the feed
  507.     @returnsUnicode
  508.     def getDescription(self):
  509.         return u"<span />"
  510.  
  511.     ##
  512.     # Returns a link to a webpage associated with the feed
  513.     @returnsUnicode
  514.     def getLink(self):
  515.         return self.ufeed.getBaseHref()
  516.  
  517.     ##
  518.     # Returns the URL of the library associated with the feed
  519.     @returnsUnicode
  520.     def getLibraryLink(self):
  521.         return u""
  522.  
  523.     ##
  524.     # Returns the URL of a thumbnail associated with the feed
  525.     @returnsUnicode
  526.     def getThumbnailURL(self):
  527.         return self.thumbURL
  528.  
  529.     ##
  530.     # Returns URL of license assocaited with the feed
  531.     @returnsUnicode
  532.     def getLicense(self):
  533.         return u""
  534.  
  535.     ##
  536.     # Returns the number of new items with the feed
  537.     def getNewItems(self):
  538.         self.ufeed.confirmDBThread()
  539.         count = 0
  540.         for item in self.items:
  541.             try:
  542.                 if item.getState() == u'newly-downloaded':
  543.                     count += 1
  544.             except:
  545.                 pass
  546.         return count
  547.  
  548.     def onRestore(self):        
  549.         self.updating = False
  550.         self.calc_item_list()
  551.  
  552.     def onRemove(self):
  553.         """Called when the feed uses this FeedImpl is removed from the DB.
  554.         subclasses can perform cleanup here."""
  555.         pass
  556.  
  557.     def __str__(self):
  558.         return "FeedImpl - %s" % self.getTitle()
  559.  
  560. ##
  561. # This class is a magic class that can become any type of feed it wants
  562. #
  563. # It works by passing on attributes to the actual feed.
  564. class Feed(DDBObject):
  565.     ICON_CACHE_SIZES = [
  566.         (20, 20),
  567.         (76, 76),
  568.     ] + Item.ICON_CACHE_SIZES
  569.  
  570.     def __init__(self,url, initiallyAutoDownloadable=True):
  571.         DDBObject.__init__(self, add=False)
  572.         checkU(url)
  573.         self.autoDownloadable = initiallyAutoDownloadable
  574.         self.getEverything = False
  575.         self.maxNew = 3
  576.         self.expire = u"system"
  577.         self.expireTime = None
  578.         self.fallBehind = -1
  579.  
  580.         self.origURL = url
  581.         self.errorState = False
  582.         self.loading = True
  583.         self.actualFeed = FeedImpl(url,self)
  584.         self.iconCache = IconCache(self, is_vital = True)
  585.         self.informOnError = True
  586.         self.folder_id = None
  587.         self.searchTerm = None
  588.         self.userTitle = None
  589.         self._initRestore()
  590.         self.dd.addAfterCursor(self)
  591.         self.generateFeed(True)
  592.  
  593.     def signalChange (self, needsSave=True, needsSignalFolder=False):
  594.         if needsSignalFolder:
  595.             folder = self.getFolder()
  596.             if folder:
  597.                 folder.signalChange(needsSave=False)
  598.         DDBObject.signalChange (self, needsSave=needsSave)
  599.  
  600.     def _initRestore(self):
  601.         self.download = None
  602.         self.blinking = False
  603.         self.itemSort = sorts.ItemSort()
  604.         self.itemSortDownloading = sorts.ItemSort()
  605.         self.itemSortWatchable = sorts.ItemSortUnwatchedFirst()
  606.         self.inlineSearchTerm = None
  607.  
  608.     isBlinking, setBlinking = makeSimpleGetSet('blinking',
  609.             changeNeedsSave=False)
  610.  
  611.     def setInlineSearchTerm(self, term):
  612.         self.inlineSearchTerm = term
  613.  
  614.     def blink(self):
  615.         self.setBlinking(True)
  616.         def timeout():
  617.             if self.idExists():
  618.                 self.setBlinking(False)
  619.         eventloop.addTimeout(0.5, timeout, 'unblink feed')
  620.  
  621.     # Returns the ID of this feed. Deprecated.
  622.     def getFeedID(self):
  623.         return self.getID()
  624.  
  625.     def getID(self):
  626.         return DDBObject.getID(self)
  627.  
  628.     def hasError(self):
  629.         self.confirmDBThread()
  630.         return self.errorState
  631.  
  632.     @returnsUnicode
  633.     def getOriginalURL(self):
  634.         self.confirmDBThread()
  635.         return self.origURL
  636.  
  637.     @returnsUnicode
  638.     def getSearchTerm(self):
  639.         self.confirmDBThread()
  640.         return self.searchTerm
  641.  
  642.     @returnsUnicode
  643.     def getError(self):
  644.         return u"Could not load feed"
  645.  
  646.     def isUpdating(self):
  647.         return self.loading or (self.actualFeed and self.actualFeed.updating)
  648.  
  649.     def isScraped(self):
  650.         return isinstance(self.actualFeed, ScraperFeedImpl)
  651.  
  652.     @returnsUnicode
  653.     def getTitle(self):
  654.         if self.userTitle is None:
  655.             title = self.actualFeed.getTitle()
  656.             if self.searchTerm is not None:
  657.                 title = u"'%s' on %s" % (self.searchTerm, title)
  658.             return title
  659.         else:
  660.             return self.userTitle
  661.  
  662.     def setTitle(self, title):
  663.         self.confirmDBThread()
  664.         self.userTitle = title
  665.         self.signalChange()
  666.  
  667.     def unsetTitle(self):
  668.         self.setTitle(None)
  669.  
  670.     @returnsUnicode
  671.     def getAutoDownloadMode(self):
  672.         self.confirmDBThread()
  673.         if self.autoDownloadable:
  674.             if self.getEverything:
  675.                 return u'all'
  676.             else:
  677.                 return u'new'
  678.         else:
  679.             return u'off'
  680.  
  681.     def setAutoDownloadMode(self, mode):
  682.         if mode == u'all':
  683.             self.setGetEverything(True)
  684.             self.setAutoDownloadable(True)
  685.         elif mode == u'new':
  686.             self.setGetEverything(False)
  687.             self.setAutoDownloadable(True)
  688.         elif mode == u'off':
  689.             self.setAutoDownloadable(False)
  690.         else:
  691.             raise ValueError("Bad auto-download mode: %s" % mode)
  692.  
  693.     def getCurrentAutoDownloadableItems(self):
  694.         auto = set()
  695.         for item in self.items:
  696.             if item.isPendingAutoDownload():
  697.                 auto.add(item)
  698.         return auto
  699.  
  700.     ##
  701.     # Switch the auto-downloadable state
  702.     def setAutoDownloadable(self, automatic):
  703.         self.confirmDBThread()
  704.         if self.autoDownloadable == automatic:
  705.             return
  706.         self.autoDownloadable = automatic
  707.  
  708.         if self.autoDownloadable:
  709.             # When turning on auto-download, existing items shouldn't be
  710.             # considered "new"
  711.             for item in self.items:
  712.                 if item.eligibleForAutoDownload:
  713.                     item.eligibleForAutoDownload = False
  714.                     item.signalChange()
  715.  
  716.         for item in self.items:
  717.             if item.isEligibleForAutoDownload():
  718.                 item.signalChange(needsSave=False)
  719.  
  720.         self.signalChange()
  721.  
  722.     ##
  723.     # Sets the 'getEverything' attribute, True or False
  724.     def setGetEverything(self, everything):
  725.         self.confirmDBThread()
  726.         if everything == self.getEverything:
  727.             return
  728.         if not self.autoDownloadable:
  729.             self.getEverything = everything
  730.             self.signalChange()
  731.             return
  732.  
  733.         updates = set()
  734.         if everything:
  735.             for item in self.items:
  736.                 if not item.isEligibleForAutoDownload():
  737.                     updates.add(item)
  738.         else:
  739.             for item in self.items:
  740.                 if item.isEligibleForAutoDownload():
  741.                     updates.add(item)
  742.  
  743.         self.getEverything = everything
  744.         self.signalChange()
  745.  
  746.         if everything:
  747.             for item in updates:
  748.                 if item.isEligibleForAutoDownload():
  749.                     item.signalChange(needsSave=False)
  750.         else:
  751.             for item in updates:
  752.                 if not item.isEligibleForAutoDownload():
  753.                     item.signalChange(needsSave=False)
  754.  
  755.     ##
  756.     # Sets the expiration attributes. Valid types are 'system', 'feed' and 'never'
  757.     # Expiration time is in hour(s).
  758.     def setExpiration(self, type, time):
  759.         self.confirmDBThread()
  760.         self.expire = type
  761.         self.expireTime = timedelta(hours=time)
  762.  
  763.         if self.expire == "never":
  764.             for item in self.items:
  765.                 if item.isDownloaded():
  766.                     item.save()
  767.  
  768.         self.signalChange()
  769.         for item in self.items:
  770.             item.signalChange(needsSave=False)
  771.  
  772.     ##
  773.     # Sets the maxNew attributes. -1 means unlimited.
  774.     def setMaxNew(self, maxNew):
  775.         self.confirmDBThread()
  776.         oldMaxNew = self.maxNew
  777.         self.maxNew = maxNew
  778.         self.signalChange()
  779. #        for item in self.items:
  780. #            item.signalChange(needsSave=False)
  781.         if self.maxNew >= oldMaxNew or self.maxNew < 0:
  782.             import autodler
  783.             autodler.autoDownloader.startDownloads()
  784.  
  785.     def makeContextMenu(self, templateName, view):
  786.         items = [
  787.             (self.update, _('Update Channel Now')),
  788.             (lambda: app.delegate.copyTextToClipboard(self.getURL()),
  789.                 _('Copy URL to clipboard')),
  790.             (self.rename, _('Rename Channel')),
  791.         ]
  792.  
  793.         if self.userTitle:
  794.             items.append((self.unsetTitle, _('Revert Title to Default')))
  795.         items.append((lambda: app.controller.removeFeed(self), _('Remove')))
  796.         return menu.makeMenu(items)
  797.  
  798.     def rename(self):
  799.         title = _("Rename Channel")
  800.         text = _("Enter a new name for the channel %s" % self.getTitle())
  801.         def callback(dialog):
  802.             if self.idExists() and dialog.choice == dialogs.BUTTON_OK:
  803.                 self.setTitle(dialog.value)
  804.         dialogs.TextEntryDialog(title, text, dialogs.BUTTON_OK,
  805.             dialogs.BUTTON_CANCEL, prefillCallback=lambda:self.getTitle()).run(callback)
  806.  
  807.     def update(self):
  808.         self.confirmDBThread()
  809.         if not self.idExists():
  810.             return
  811.         if self.loading:
  812.             return
  813.         elif self.errorState:
  814.             self.loading = True
  815.             self.errorState = False
  816.             self.signalChange()
  817.             return self.generateFeed()
  818.         self.actualFeed.update()
  819.  
  820.     def getFolder(self):
  821.         self.confirmDBThread()
  822.         if self.folder_id is not None:
  823.             return self.dd.getObjectByID(self.folder_id)
  824.         else:
  825.             return None
  826.  
  827.     def setFolder(self, newFolder):
  828.         self.confirmDBThread()
  829.         oldFolder = self.getFolder()
  830.         if newFolder is not None:
  831.             self.folder_id = newFolder.getID()
  832.         else:
  833.             self.folder_id = None
  834.         self.signalChange()
  835.         for item in self.items:
  836.             item.signalChange(needsSave=False, needsUpdateXML=False)
  837.         if newFolder:
  838.             newFolder.signalChange(needsSave=False)
  839.         if oldFolder:
  840.             oldFolder.signalChange(needsSave=False)
  841.  
  842.     def generateFeed(self, removeOnError=False):
  843.         newFeed = None
  844.         if (self.origURL == u"dtv:directoryfeed"):
  845.             newFeed = DirectoryFeedImpl(self)
  846.         elif (self.origURL.startswith(u"dtv:directoryfeed:")):
  847.             url = self.origURL[len(u"dtv:directoryfeed:"):]
  848.             dir = unmakeURLSafe(url)
  849.             newFeed = DirectoryWatchFeedImpl(self, dir)
  850.         elif (self.origURL == u"dtv:search"):
  851.             newFeed = SearchFeedImpl(self)
  852.         elif (self.origURL == u"dtv:searchDownloads"):
  853.             newFeed = SearchDownloadsFeedImpl(self)
  854.         elif (self.origURL == u"dtv:manualFeed"):
  855.             newFeed = ManualFeedImpl(self)
  856.         elif (self.origURL == u"dtv:singleFeed"):
  857.             newFeed = SingleFeedImpl(self)
  858.         elif (self.origURL.startswith (u"dtv:searchTerm:")):
  859.  
  860.             url = self.origURL[len(u"dtv:searchTerm:"):]
  861.             (url, search) = url.rsplit("?", 1)
  862.             url = urldecode(url)
  863.             # search terms encoded as utf-8, but our URL attribute is then
  864.             # converted to unicode.  So we need to:
  865.             #  - convert the unicode to a raw string
  866.             #  - urldecode that string
  867.             #  - utf-8 decode the result.
  868.             search = urldecode(search.encode('ascii')).decode('utf-8')
  869.             self.searchTerm = search
  870.             self.download = grabURL(url,
  871.                     lambda info:self._generateFeedCallback(info, removeOnError),
  872.                     lambda error:self._generateFeedErrback(error, removeOnError),
  873.                     defaultMimeType=u'application/rss+xml')
  874.         else:
  875.             self.download = grabURL(self.origURL,
  876.                     lambda info:self._generateFeedCallback(info, removeOnError),
  877.                     lambda error:self._generateFeedErrback(error, removeOnError),
  878.                     defaultMimeType=u'application/rss+xml')
  879.             logging.debug ("added async callback to create feed %s", self.origURL)
  880.         if newFeed:
  881.             self.actualFeed = newFeed
  882.             self.loading = False
  883.  
  884.             self.signalChange()
  885.  
  886.     def _handleFeedLoadingError(self, errorDescription):
  887.         self.download = None
  888.         self.errorState = True
  889.         self.loading = False
  890.         self.signalChange()
  891.         if self.informOnError:
  892.             title = _('Error loading feed')
  893.             description = _("Couldn't load the feed at %s (%s).") % (
  894.                     self.url, errorDescription)
  895.             description += "\n\n"
  896.             description += _("Would you like to keep the feed?")
  897.             d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_KEEP,
  898.                     dialogs.BUTTON_DELETE)
  899.             def callback(dialog):
  900.                 if dialog.choice == dialogs.BUTTON_DELETE and self.idExists():
  901.                     self.remove()
  902.             d.run(callback)
  903.             self.informOnError = False
  904.         delay = config.get(prefs.CHECK_CHANNELS_EVERY_X_MN)
  905.         eventloop.addTimeout(delay, self.update, "update failed feed")
  906.  
  907.     def _generateFeedErrback(self, error, removeOnError):
  908.         if not self.idExists():
  909.             return
  910.         logging.info ("Warning couldn't load feed at %s (%s)",
  911.                       self.origURL, error)
  912.         self._handleFeedLoadingError(error.getFriendlyDescription())
  913.  
  914.     def _generateFeedCallback(self, info, removeOnError):
  915.         """This is called by grabURL to generate a feed based on
  916.         the type of data found at the given URL
  917.         """
  918.         # FIXME: This probably should be split up a bit. The logic is
  919.         #        a bit daunting
  920.  
  921.  
  922.         # Note that all of the raw XML and HTML in this function is in
  923.         # byte string format
  924.  
  925.         if not self.idExists():
  926.             return
  927.         self.download = None
  928.         modified = unicodify(info.get('last-modified'))
  929.         etag = unicodify(info.get('etag'))
  930.         contentType = unicodify(info.get('content-type', u'text/html'))
  931.         
  932.         # Some smarty pants serve RSS feeds with a text/html content-type...
  933.         # So let's do some really simple sniffing first.
  934.         apparentlyRSS = re.compile(r'<\?xml.*\?>\s*<rss').match(info['body']) is not None
  935.  
  936.         #Definitely an HTML feed
  937.         if (contentType.startswith(u'text/html') or 
  938.             contentType.startswith(u'application/xhtml+xml')) and not apparentlyRSS:
  939.             #print "Scraping HTML"
  940.             html = info['body']
  941.             if info.has_key('charset'):
  942.                 html = fixHTMLHeader(html,info['charset'])
  943.                 charset = unicodify(info['charset'])
  944.             else:
  945.                 charset = None
  946.             self.askForScrape(info, html, charset)
  947.         #It's some sort of feed we don't know how to scrape
  948.         elif (contentType.startswith(u'application/rdf+xml') or
  949.               contentType.startswith(u'application/atom+xml')):
  950.             #print "ATOM or RDF"
  951.             html = info['body']
  952.             if info.has_key('charset'):
  953.                 xmldata = fixXMLHeader(html,info['charset'])
  954.             else:
  955.                 xmldata = html
  956.             self.finishGenerateFeed(RSSFeedImpl(unicodify(info['updated-url']),
  957.                 initialHTML=xmldata,etag=etag,modified=modified, ufeed=self))
  958.             # If it's not HTML, we can't be sure what it is.
  959.             #
  960.             # If we get generic XML, it's probably RSS, but it still could
  961.             # be XHTML.
  962.             #
  963.             # application/rss+xml links are definitely feeds. However, they
  964.             # might be pre-enclosure RSS, so we still have to download them
  965.             # and parse them before we can deal with them correctly.
  966.         elif (apparentlyRSS or
  967.               contentType.startswith(u'application/rss+xml') or
  968.               contentType.startswith(u'application/podcast+xml') or
  969.               contentType.startswith(u'text/xml') or 
  970.               contentType.startswith(u'application/xml') or
  971.               (contentType.startswith(u'text/plain') and
  972.                (unicodify(info['updated-url']).endswith(u'.xml') or
  973.                 unicodify(info['updated-url']).endswith(u'.rss')))):
  974.             #print " It's doesn't look like HTML..."
  975.             html = info["body"]
  976.             if info.has_key('charset'):
  977.                 xmldata = fixXMLHeader(html,info['charset'])
  978.                 html = fixHTMLHeader(html,info['charset'])
  979.                 charset = unicodify(info['charset'])
  980.             else:
  981.                 xmldata = html
  982.                 charset = None
  983.             # FIXME html and xmldata can be non-unicode at this point
  984.             parser = xml.sax.make_parser()
  985.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  986.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  987.             except: pass
  988.             handler = RSSLinkGrabber(unicodify(info['redirected-url']),charset)
  989.             parser.setContentHandler(handler)
  990.             parser.setErrorHandler(handler)
  991.             try:
  992.                 parser.parse(StringIO(xmldata))
  993.             except UnicodeDecodeError:
  994.                 logging.exception ("Unicode issue parsing... %s", xmldata[0:300])
  995.                 self.finishGenerateFeed(None)
  996.                 if removeOnError:
  997.                     self.remove()
  998.             except:
  999.                 #it doesn't parse as RSS, so it must be HTML
  1000.                 #print " Nevermind! it's HTML"
  1001.                 self.askForScrape(info, html, charset)
  1002.             else:
  1003.                 #print " It's RSS with enclosures"
  1004.                 self.finishGenerateFeed(RSSFeedImpl(
  1005.                     unicodify(info['updated-url']),
  1006.                     initialHTML=xmldata, etag=etag, modified=modified,
  1007.                     ufeed=self))
  1008.         else:
  1009.             self._handleFeedLoadingError(_("Bad content-type"))
  1010.  
  1011.     def finishGenerateFeed(self, feedImpl):
  1012.         self.confirmDBThread()
  1013.         self.loading = False
  1014.         if feedImpl is not None:
  1015.             self.actualFeed = feedImpl
  1016.             self.errorState = False
  1017.         else:
  1018.             self.errorState = True
  1019.         self.signalChange()
  1020.  
  1021.     def askForScrape(self, info, initialHTML, charset):
  1022.         title = Template(_("Channel is not compatible with $shortAppName!")).substitute(shortAppName=config.get(prefs.SHORT_APP_NAME))
  1023.         descriptionTemplate = Template(_("""\
  1024. But we'll try our best to grab the files. It may take extra time to list the \
  1025. videos, and descriptions may look funny.  Please contact the publishers of \
  1026. $url and ask if they can supply a feed in a format that will work with \
  1027. $shortAppName.\n\nDo you want to try to load this channel anyway?"""))
  1028.         description = descriptionTemplate.substitute(url=info['updated-url'],
  1029.                                 shortAppName=config.get(prefs.SHORT_APP_NAME))
  1030.         dialog = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_YES,
  1031.                 dialogs.BUTTON_NO)
  1032.  
  1033.         def callback(dialog):
  1034.             if not self.idExists():
  1035.                 return
  1036.             if dialog.choice == dialogs.BUTTON_YES:
  1037.                 uinfo = unicodify(info)
  1038.                 impl = ScraperFeedImpl(uinfo['updated-url'],
  1039.                     initialHTML=initialHTML, etag=uinfo.get('etag'),
  1040.                     modified=uinfo.get('modified'), charset=charset,
  1041.                     ufeed=self) 
  1042.                 self.finishGenerateFeed(impl)
  1043.             else:
  1044.                 self.remove()
  1045.         dialog.run(callback)
  1046.  
  1047.     def getActualFeed(self):
  1048.         return self.actualFeed
  1049.  
  1050.     def __getattr__(self,attr):
  1051.         return getattr(self.actualFeed,attr)
  1052.  
  1053.     def remove(self, moveItemsTo=None):
  1054.         """Remove the feed.  If moveItemsTo is None (the default), the items
  1055.         in this feed will be removed too.  If moveItemsTo is given, the items
  1056.         in this feed will be moved to that feed.
  1057.         """
  1058.  
  1059.         self.confirmDBThread()
  1060.  
  1061.         if isinstance (self.actualFeed, DirectoryWatchFeedImpl):
  1062.             moveItemsTo = None
  1063.         self.cancelUpdateEvents()
  1064.         if self.download is not None:
  1065.             self.download.cancel()
  1066.             self.download = None
  1067.         for item in self.items:
  1068.             if moveItemsTo is not None and item.isDownloaded():
  1069.                 item.setFeed(moveItemsTo.getID())
  1070.             else:
  1071.                 item.remove()
  1072.         if self.iconCache is not None:
  1073.             self.iconCache.remove()
  1074.             self.iconCache = None
  1075.         DDBObject.remove(self)
  1076.         self.actualFeed.onRemove()
  1077.  
  1078.     @returnsUnicode
  1079.     def getThumbnail(self):
  1080.         self.confirmDBThread()
  1081.         if self.iconCache and self.iconCache.isValid():
  1082.             path = self.iconCache.getResizedFilename(76, 76)
  1083.             return resources.absoluteUrl(path)
  1084.         else:
  1085.             return defaultFeedIconURL()
  1086.  
  1087.     @returnsUnicode
  1088.     def getTablistThumbnail(self):
  1089.         self.confirmDBThread()
  1090.         if self.iconCache and self.iconCache.isValid():
  1091.             path = self.iconCache.getResizedFilename(20, 20)
  1092.             return resources.absoluteUrl(path)
  1093.         else:
  1094.             return defaultFeedIconURLTablist()
  1095.  
  1096.     @returnsUnicode
  1097.     def getItemThumbnail(self, width, height):
  1098.         self.confirmDBThread()
  1099.         if self.iconCache and self.iconCache.isValid():
  1100.             path = self.iconCache.getResizedFilename(width, height)
  1101.             return resources.absoluteUrl(path)
  1102.         else:
  1103.             return None
  1104.  
  1105.     def hasDownloadedItems(self):
  1106.         self.confirmDBThread()
  1107.         for item in self.items:
  1108.             if item.isDownloaded():
  1109.                 return True
  1110.         return False
  1111.  
  1112.     def hasDownloadingItems(self):
  1113.         self.confirmDBThread()
  1114.         for item in self.items:
  1115.             if item.getState() in (u'downloading', u'paused'):
  1116.                 return True
  1117.         return False
  1118.  
  1119.     def updateIcons(self):
  1120.         iconCacheUpdater.clearVital()
  1121.         for item in self.items:
  1122.             item.iconCache.requestUpdate(True)
  1123.         for feed in views.feeds:
  1124.             feed.iconCache.requestUpdate(True)
  1125.  
  1126.     @returnsUnicode
  1127.     def getDragDestType(self):
  1128.         self.confirmDBThread()
  1129.         if self.folder_id is not None:
  1130.             return u'channel'
  1131.         else:
  1132.             return u'channel:channelfolder'
  1133.  
  1134.     def onRestore(self):
  1135.         if (self.iconCache == None):
  1136.             self.iconCache = IconCache (self, is_vital = True)
  1137.         else:
  1138.             self.iconCache.dbItem = self
  1139.             self.iconCache.requestUpdate(True)
  1140.         self.informOnError = False
  1141.         self._initRestore()
  1142.         if self.actualFeed.__class__ == FeedImpl:
  1143.             # Our initial FeedImpl was never updated, call generateFeed again
  1144.             self.loading = True
  1145.             eventloop.addIdle(lambda:self.generateFeed(True), "generateFeed")
  1146.  
  1147.     def __str__(self):
  1148.         return "Feed - %s" % self.getTitle()
  1149.  
  1150. def _entry_equal(a, b):
  1151.     if type(a) == list and type(b) == list:
  1152.         if len(a) != len(b):
  1153.             return False
  1154.         for i in xrange (len(a)):
  1155.             if not _entry_equal(a[i], b[i]):
  1156.                 return False
  1157.         return True
  1158.     try:
  1159.         return a.equal(b)
  1160.     except:
  1161.         try:
  1162.             return b.equal(a)
  1163.         except:
  1164.             return a == b
  1165.  
  1166. class RSSFeedImpl(FeedImpl):
  1167.     firstImageRE = re.compile('\<\s*img\s+[^>]*src\s*=\s*"(.*?)"[^>]*\>',re.I|re.M)
  1168.     
  1169.     def __init__(self,url,ufeed,title = None,initialHTML = None, etag = None, modified = None, visible=True):
  1170.         FeedImpl.__init__(self,url,ufeed,title,visible=visible)
  1171.         self.initialHTML = initialHTML
  1172.         self.etag = etag
  1173.         self.modified = modified
  1174.         self.download = None
  1175.         self.scheduleUpdateEvents(0)
  1176.  
  1177.     @returnsUnicode
  1178.     def getBaseHref(self):
  1179.         try:
  1180.             return escape(self.parsed.link)
  1181.         except:
  1182.             return FeedImpl.getBaseHref(self)
  1183.  
  1184.     ##
  1185.     # Returns the description of the feed
  1186.     @returnsUnicode
  1187.     def getDescription(self):
  1188.         self.ufeed.confirmDBThread()
  1189.         try:
  1190.             return xhtmlify(u'<span>'+unescape(self.parsed.feed.description)+u'</span>')
  1191.         except:
  1192.             return u"<span />"
  1193.  
  1194.     ##
  1195.     # Returns a link to a webpage associated with the feed
  1196.     @returnsUnicode
  1197.     def getLink(self):
  1198.         self.ufeed.confirmDBThread()
  1199.         try:
  1200.             return self.parsed.link
  1201.         except:
  1202.             return u""
  1203.  
  1204.     ##
  1205.     # Returns the URL of the library associated with the feed
  1206.     @returnsUnicode
  1207.     def getLibraryLink(self):
  1208.         self.ufeed.confirmDBThread()
  1209.         try:
  1210.             return self.parsed.libraryLink
  1211.         except:
  1212.             return u""
  1213.  
  1214.     def feedparser_finished (self):
  1215.         self.updating = False
  1216.         self.ufeed.signalChange(needsSave=False)
  1217.         self.scheduleUpdateEvents(-1)
  1218.  
  1219.     def feedparser_errback (self, e):
  1220.         if not self.ufeed.idExists():
  1221.             return
  1222.         logging.info ("Error updating feed: %s: %s", self.url, e)
  1223.         self.updating = False
  1224.         self.ufeed.signalChange()
  1225.         self.scheduleUpdateEvents(-1)
  1226.  
  1227.     def feedparser_callback (self, parsed):
  1228.         self.ufeed.confirmDBThread()
  1229.         if not self.ufeed.idExists():
  1230.             return
  1231.         start = clock()
  1232.         self.updateUsingParsed(parsed)
  1233.         self.feedparser_finished()
  1234.         end = clock()
  1235.         if end - start > 1.0:
  1236.             logging.timing ("feed update for: %s too slow (%.3f secs)", self.url, end - start)
  1237.  
  1238.     def call_feedparser (self, html):
  1239.         self.ufeed.confirmDBThread()
  1240.         in_thread = False
  1241.         if in_thread:
  1242.             try:
  1243.                 parsed = feedparser.parse(html)
  1244.                 self.updateUsingParsed(parsed)
  1245.             except:
  1246.                 logging.warning ("Error updating feed: %s", self.url)
  1247.                 self.updating = False
  1248.                 self.ufeed.signalChange(needsSave=False)
  1249.                 raise
  1250.             self.feedparser_finished()
  1251.         else:
  1252.             eventloop.callInThread (self.feedparser_callback, self.feedparser_errback, feedparser.parse, "Feedparser callback - %s" % self.url, html)
  1253.  
  1254.     ##
  1255.     # Updates a feed
  1256.     def update(self):
  1257.         self.ufeed.confirmDBThread()
  1258.         if not self.ufeed.idExists():
  1259.             return
  1260.         if self.updating:
  1261.             return
  1262.         else:
  1263.             self.updating = True
  1264.             self.ufeed.signalChange(needsSave=False)
  1265.         if hasattr(self, 'initialHTML') and self.initialHTML is not None:
  1266.             html = self.initialHTML
  1267.             self.initialHTML = None
  1268.             self.call_feedparser (html)
  1269.         else:
  1270.             try:
  1271.                 etag = self.etag
  1272.             except:
  1273.                 etag = None
  1274.             try:
  1275.                 modified = self.modified
  1276.             except:
  1277.                 modified = None
  1278.             self.download = grabURL(self.url, self._updateCallback,
  1279.                     self._updateErrback, etag=etag,modified=modified,defaultMimeType=u'application/rss+xml',)
  1280.  
  1281.     def _updateErrback(self, error):
  1282.         if not self.ufeed.idExists():
  1283.             return
  1284.         logging.info ("WARNING: error in Feed.update for %s -- %s", self.ufeed, error)
  1285.         self.scheduleUpdateEvents(-1)
  1286.         self.updating = False
  1287.         self.ufeed.signalChange(needsSave=False)
  1288.  
  1289.     def _updateCallback(self,info):
  1290.         if not self.ufeed.idExists():
  1291.             return
  1292.         if info.get('status') == 304:
  1293.             self.scheduleUpdateEvents(-1)
  1294.             self.updating = False
  1295.             self.ufeed.signalChange()
  1296.             return
  1297.         html = info['body']
  1298.         if info.has_key('charset'):
  1299.             html = fixXMLHeader(html,info['charset'])
  1300.  
  1301.         # FIXME HTML can be non-unicode here --NN        
  1302.         self.url = unicodify(info['updated-url'])
  1303.         if info.has_key('etag'):
  1304.             self.etag = unicodify(info['etag'])
  1305.         if info.has_key('last-modified'):
  1306.             self.modified = unicodify(info['last-modified'])
  1307.         self.call_feedparser (html)
  1308.  
  1309.     def _handleNewEntryForItem(self, item, entry):
  1310.         """Handle when we get a different entry for an item.
  1311.  
  1312.         This happens when the feed sets the RSS GUID attribute, then changes
  1313.         the entry for it.  Most of the time we will just update the item, but
  1314.         if the user has already downloaded the item then we need to make sure
  1315.         that we don't throw away the download.
  1316.         """
  1317.  
  1318.         videoEnc = getFirstVideoEnclosure(entry)
  1319.         if videoEnc is not None:
  1320.             entryURL = videoEnc.get('url')
  1321.         else:
  1322.             entryURL = None
  1323.         if item.isDownloaded() and item.getURL() != entryURL:
  1324.             item.removeRSSID()
  1325.             self._handleNewEntry(entry)
  1326.         else:
  1327.             item.update(entry)
  1328.  
  1329.     def _handleNewEntry(self, entry):
  1330.         """Handle getting a new entry from a feed."""
  1331.         item = Item(entry, feed_id=self.ufeed.id)
  1332.         if not filters.matchingItems(item, self.ufeed.searchTerm):
  1333.             item.remove()
  1334.  
  1335.     def updateUsingParsed(self, parsed):
  1336.         """Update the feed using parsed XML passed in"""
  1337.         self.parsed = unicodify(parsed)
  1338.  
  1339.         # This is a HACK for Yahoo! search which doesn't provide
  1340.         # enclosures
  1341.         for entry in parsed['entries']:
  1342.             if 'enclosures' not in entry:
  1343.                 try:
  1344.                     url = entry['link']
  1345.                 except:
  1346.                     continue
  1347.                 mimetype = filetypes.guessMimeType(url)
  1348.                 if mimetype is not None:
  1349.                     entry['enclosures'] = [{'url':toUni(url), 'type':toUni(mimetype)}]
  1350.                 else:
  1351.                     logging.info('unknown url type %s, not generating enclosure' % url)
  1352.  
  1353.         try:
  1354.             self.title = self.parsed["feed"]["title"]
  1355.         except KeyError:
  1356.             try:
  1357.                 self.title = self.parsed["channel"]["title"]
  1358.             except KeyError:
  1359.                 pass
  1360.         if (self.parsed.feed.has_key('image') and 
  1361.             self.parsed.feed.image.has_key('url')):
  1362.             self.thumbURL = self.parsed.feed.image.url
  1363.             self.ufeed.iconCache.requestUpdate(is_vital=True)
  1364.         items_byid = {}
  1365.         items_byURLTitle = {}
  1366.         items_nokey = []
  1367.         old_items = set()
  1368.         for item in self.items:
  1369.             old_items.add(item)
  1370.             try:
  1371.                 items_byid[item.getRSSID()] = item
  1372.             except KeyError:
  1373.                 items_nokey.append (item)
  1374.             entry = item.getRSSEntry()
  1375.             videoEnc = getFirstVideoEnclosure(entry)
  1376.             if videoEnc is not None:
  1377.                 entryURL = videoEnc.get('url')
  1378.             else:
  1379.                 entryURL = None
  1380.             title = entry.get("title")
  1381.             if title is not None or entryURL is not None:
  1382.                 items_byURLTitle[(entryURL, title)] = item
  1383.         for entry in self.parsed.entries:
  1384.             entry = self.addScrapedThumbnail(entry)
  1385.             new = True
  1386.             if entry.has_key("id"):
  1387.                 id = entry["id"]
  1388.                 if items_byid.has_key (id):
  1389.                     item = items_byid[id]
  1390.                     if not _entry_equal(entry, item.getRSSEntry()):
  1391.                         self._handleNewEntryForItem(item, entry)
  1392.                     new = False
  1393.                     old_items.discard(item)
  1394.             if new:
  1395.                 videoEnc = getFirstVideoEnclosure(entry)
  1396.                 if videoEnc is not None:
  1397.                     entryURL = videoEnc.get('url')
  1398.                 else:
  1399.                     entryURL = None
  1400.                 title = entry.get("title")
  1401.                 if title is not None or entryURL is not None:
  1402.                     if items_byURLTitle.has_key ((entryURL, title)):
  1403.                         item = items_byURLTitle[(entryURL, title)]
  1404.                         if not _entry_equal(entry, item.getRSSEntry()):
  1405.                             self._handleNewEntryForItem(item, entry)
  1406.                         new = False
  1407.                         old_items.discard(item)
  1408.             if new:
  1409.                 for item in items_nokey:
  1410.                     if _entry_equal(entry, item.getRSSEntry()):
  1411.                         new = False
  1412.                     else:
  1413.                         try:
  1414.                             if _entry_equal (entry["enclosures"], item.getRSSEntry()["enclosures"]):
  1415.                                 self._handleNewEntryForItem(item, entry)
  1416.                                 new = False
  1417.                                 old_items.discard(item)
  1418.                         except:
  1419.                             pass
  1420.             if (new and entry.has_key('enclosures') and
  1421.                     getFirstVideoEnclosure(entry) != None):
  1422.                 self._handleNewEntry(entry)
  1423.         try:
  1424.             updateFreq = self.parsed["feed"]["ttl"]
  1425.         except KeyError:
  1426.             updateFreq = 0
  1427.         self.setUpdateFrequency(updateFreq)
  1428.         
  1429.         if self.initialUpdate:
  1430.             self.initialUpdate = False
  1431.             startfrom = None
  1432.             itemToUpdate = None
  1433.             for item in self.items:
  1434.                 itemTime = item.getPubDateParsed()
  1435.                 if startfrom is None or itemTime > startfrom:
  1436.                     startfrom = itemTime
  1437.                     itemToUpdate = item
  1438.             for item in self.items:
  1439.                 if item == itemToUpdate:
  1440.                     item.eligibleForAutoDownload = True
  1441.                 else:
  1442.                     item.eligibleForAutoDownload = False
  1443.                 item.signalChange()
  1444.             self.ufeed.signalChange()
  1445.  
  1446.         self.truncateOldItems(old_items)
  1447.  
  1448.     def truncateOldItems(self, old_items):
  1449.         """Truncate items so that the number of items in this feed doesn't
  1450.         exceed prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS.
  1451.  
  1452.         old_items should be an iterable that contains items that aren't in the
  1453.         feed anymore.
  1454.  
  1455.         Items are only truncated if they don't exist in the feed anymore, and
  1456.         if the user hasn't downloaded them.
  1457.         """
  1458.         limit = config.get(prefs.TRUNCATE_CHANNEL_AFTER_X_ITEMS)
  1459.         extra = len(self.items) - limit
  1460.         if extra <= 0:
  1461.             return
  1462.  
  1463.         candidates = []
  1464.         for item in old_items:
  1465.             if item.downloader is None:
  1466.                 candidates.append((item.creationTime, item))
  1467.         candidates.sort()
  1468.         for time, item in candidates[:extra]:
  1469.             item.remove()
  1470.  
  1471.     def addScrapedThumbnail(self,entry):
  1472.         # skip this if the entry already has a thumbnail.
  1473.         if entry.has_key('thumbnail'):
  1474.             return entry
  1475.         if entry.has_key('enclosures'):
  1476.             for enc in entry['enclosures']:
  1477.                 if enc.has_key('thumbnail'):
  1478.                     return entry
  1479.         # try to scape the thumbnail from the description.
  1480.         if not entry.has_key('description'):
  1481.             return entry
  1482.         desc = RSSFeedImpl.firstImageRE.search(unescape(entry['description']))
  1483.         if not desc is None:
  1484.             entry['thumbnail'] = FeedParserDict({'url': desc.expand("\\1")})
  1485.         return entry
  1486.  
  1487.     ##
  1488.     # Returns the URL of the license associated with the feed
  1489.     @returnsUnicode
  1490.     def getLicense(self):
  1491.         try:
  1492.             ret = self.parsed.license
  1493.         except:
  1494.             ret = u""
  1495.         return ret
  1496.  
  1497.     def onRemove(self):
  1498.         if self.download is not None:
  1499.             self.download.cancel()
  1500.             self.download = None
  1501.  
  1502.     ##
  1503.     # Called by pickle during deserialization
  1504.     def onRestore(self):
  1505.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1506.         #FIXME: the update dies if all of the items aren't restored, so we 
  1507.         # wait a little while before we start the update
  1508.         FeedImpl.onRestore(self)
  1509.         self.download = None
  1510.         self.scheduleUpdateEvents(0.1)
  1511.  
  1512.  
  1513. ##
  1514. # A DTV Collection of items -- similar to a playlist
  1515. class Collection(FeedImpl):
  1516.     def __init__(self,ufeed,title = None):
  1517.         FeedImpl.__init__(self,ufeed,url = "dtv:collection",title = title,visible = False)
  1518.  
  1519.     ##
  1520.     # Adds an item to the collection
  1521.     def addItem(self,item):
  1522.         if isinstance(item,Item):
  1523.             self.ufeed.confirmDBThread()
  1524.             self.removeItem(item)
  1525.             self.items.append(item)
  1526.             return True
  1527.         else:
  1528.             return False
  1529.  
  1530.     ##
  1531.     # Moves an item to another spot in the collection
  1532.     def moveItem(self,item,pos):
  1533.         self.ufeed.confirmDBThread()
  1534.         self.removeItem(item)
  1535.         if pos < len(self.items):
  1536.             self.items[pos:pos] = [item]
  1537.         else:
  1538.             self.items.append(item)
  1539.  
  1540.     ##
  1541.     # Removes an item from the collection
  1542.     def removeItem(self,item):
  1543.         self.ufeed.confirmDBThread()
  1544.         for x in range(0,len(self.items)):
  1545.             if self.items[x] == item:
  1546.                 self.items[x:x+1] = []
  1547.                 break
  1548.         return True
  1549.  
  1550. ##
  1551. # A feed based on un unformatted HTML or pre-enclosure RSS
  1552. class ScraperFeedImpl(FeedImpl):
  1553.     def __init__(self,url,ufeed, title = None, visible = True, initialHTML = None,etag=None,modified = None,charset = None):
  1554.         FeedImpl.__init__(self,url,ufeed,title,visible)
  1555.         self.initialHTML = initialHTML
  1556.         self.initialCharset = charset
  1557.         self.linkHistory = {}
  1558.         self.linkHistory[url] = {}
  1559.         self.tempHistory = {}
  1560.         if not etag is None:
  1561.             self.linkHistory[url]['etag'] = unicodify(etag)
  1562.         if not modified is None:
  1563.             self.linkHistory[url]['modified'] = unicodify(modified)
  1564.         self.downloads = set()
  1565.         self.setUpdateFrequency(360)
  1566.         self.scheduleUpdateEvents(0)
  1567.  
  1568.     @returnsUnicode
  1569.     def getMimeType(self,link):
  1570.         raise StandardError, "ScraperFeedImpl.getMimeType not implemented"
  1571.  
  1572.     ##
  1573.     # This puts all of the caching information in tempHistory into the
  1574.     # linkHistory. This should be called at the end of an updated so that
  1575.     # the next time we update we don't unnecessarily follow old links
  1576.     def saveCacheHistory(self):
  1577.         self.ufeed.confirmDBThread()
  1578.         for url in self.tempHistory.keys():
  1579.             self.linkHistory[url] = self.tempHistory[url]
  1580.         self.tempHistory = {}
  1581.     ##
  1582.     # grabs HTML at the given URL, then processes it
  1583.     def getHTML(self, urlList, depth = 0, linkNumber = 0, top = False):
  1584.         url = urlList.pop(0)
  1585.         #print "Grabbing %s" % url
  1586.         etag = None
  1587.         modified = None
  1588.         if self.linkHistory.has_key(url):
  1589.             if self.linkHistory[url].has_key('etag'):
  1590.                 etag = self.linkHistory[url]['etag']
  1591.             if self.linkHistory[url].has_key('modified'):
  1592.                 modified = self.linkHistory[url]['modified']
  1593.         def callback(info):
  1594.             if not self.ufeed.idExists():
  1595.                 return
  1596.             self.downloads.discard(download)
  1597.             try:
  1598.                 self.processDownloadedHTML(info, urlList, depth,linkNumber, top)
  1599.             finally:
  1600.                 self.checkDone()
  1601.         def errback(error):
  1602.             if not self.ufeed.idExists():
  1603.                 return
  1604.             self.downloads.discard(download)
  1605.             logging.info ("WARNING unhandled error for ScraperFeedImpl.getHTML: %s", error)
  1606.             self.checkDone()
  1607.         download = grabURL(url, callback, errback, etag=etag,
  1608.                 modified=modified,defaultMimeType='text/html',)
  1609.         self.downloads.add(download)
  1610.  
  1611.     def processDownloadedHTML(self, info, urlList, depth, linkNumber, top = False):
  1612.         self.ufeed.confirmDBThread()
  1613.         #print "Done grabbing %s" % info['updated-url']
  1614.         
  1615.         if not self.tempHistory.has_key(info['updated-url']):
  1616.             self.tempHistory[info['updated-url']] = {}
  1617.         if info.has_key('etag'):
  1618.             self.tempHistory[info['updated-url']]['etag'] = unicodify(info['etag'])
  1619.         if info.has_key('last-modified'):
  1620.             self.tempHistory[info['updated-url']]['modified'] = unicodify(info['last-modified'])
  1621.  
  1622.         if (info['status'] != 304) and (info.has_key('body')):
  1623.             if info.has_key('charset'):
  1624.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'],charset=info['charset'], setTitle = top)
  1625.             else:
  1626.                 subLinks = self.scrapeLinks(info['body'], info['redirected-url'], setTitle = top)
  1627.             if top:
  1628.                 self.processLinks(subLinks,0,linkNumber)
  1629.             else:
  1630.                 self.processLinks(subLinks,depth+1,linkNumber)
  1631.         if len(urlList) > 0:
  1632.             self.getHTML(urlList, depth, linkNumber)
  1633.  
  1634.     def checkDone(self):
  1635.         if len(self.downloads) == 0:
  1636.             self.saveCacheHistory()
  1637.             self.updating = False
  1638.             self.ufeed.signalChange()
  1639.             self.scheduleUpdateEvents(-1)
  1640.  
  1641.     def addVideoItem(self,link,dict,linkNumber):
  1642.         link = unicodify(link.strip())
  1643.         if dict.has_key('title'):
  1644.             title = dict['title']
  1645.         else:
  1646.             title = link
  1647.         for item in self.items:
  1648.             if item.getURL() == link:
  1649.                 return
  1650.         # Anywhere we call this, we need to convert the input back to unicode
  1651.         title = feedparser.sanitizeHTML (title, "utf-8").decode('utf-8')
  1652.         if dict.has_key('thumbnail') > 0:
  1653.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link,'thumbnail':FeedParserDict({'url':dict['thumbnail']})})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  1654.         else:
  1655.             i=Item(FeedParserDict({'title':title,'enclosures':[FeedParserDict({'url':link})]}),linkNumber = linkNumber, feed_id=self.ufeed.id)
  1656.         if self.ufeed.searchTerm is not None and not filters.matchingItems(i, self.ufeed.searchTerm):
  1657.             i.remove()
  1658.             return
  1659.  
  1660.     #FIXME: compound names for titles at each depth??
  1661.     def processLinks(self,links, depth = 0,linkNumber = 0):
  1662.         maxDepth = 2
  1663.         urls = links[0]
  1664.         links = links[1]
  1665.         # List of URLs that should be downloaded
  1666.         newURLs = []
  1667.         
  1668.         if depth<maxDepth:
  1669.             for link in urls:
  1670.                 if depth == 0:
  1671.                     linkNumber += 1
  1672.                 #print "Processing %s (%d)" % (link,linkNumber)
  1673.  
  1674.                 # FIXME: Using file extensions totally breaks the
  1675.                 # standard and won't work with Broadcast Machine or
  1676.                 # Blog Torrent. However, it's also a hell of a lot
  1677.                 # faster than checking the mime type for every single
  1678.                 # file, so for now, we're being bad boys. Uncomment
  1679.                 # the elif to make this use mime types for HTTP GET URLs
  1680.  
  1681.                 mimetype = filetypes.guessMimeType(link)
  1682.                 if mimetype is None:
  1683.                     mimetype = 'text/html'
  1684.  
  1685.                 #This is text of some sort: HTML, XML, etc.
  1686.                 if ((mimetype.startswith('text/html') or
  1687.                      mimetype.startswith('application/xhtml+xml') or 
  1688.                      mimetype.startswith('text/xml')  or
  1689.                      mimetype.startswith('application/xml') or
  1690.                      mimetype.startswith('application/rss+xml') or
  1691.                      mimetype.startswith('application/podcast+xml') or
  1692.                      mimetype.startswith('application/atom+xml') or
  1693.                      mimetype.startswith('application/rdf+xml') ) and
  1694.                     depth < maxDepth -1):
  1695.                     newURLs.append(link)
  1696.  
  1697.                 #This is a video
  1698.                 elif (mimetype.startswith('video/') or 
  1699.                       mimetype.startswith('audio/') or
  1700.                       mimetype == "application/ogg" or
  1701.                       mimetype == "application/x-annodex" or
  1702.                       mimetype == "application/x-bittorrent"):
  1703.                     self.addVideoItem(link, links[link],linkNumber)
  1704.             if len(newURLs) > 0:
  1705.                 self.getHTML(newURLs, depth, linkNumber)
  1706.  
  1707.     def onRemove(self):
  1708.         for download in self.downloads:
  1709.             logging.info ("cancling download: %s", download.url)
  1710.             download.cancel()
  1711.         self.downloads = set()
  1712.  
  1713.     #FIXME: go through and add error handling
  1714.     def update(self):
  1715.         self.ufeed.confirmDBThread()
  1716.         if not self.ufeed.idExists():
  1717.             return
  1718.         if self.updating:
  1719.             return
  1720.         else:
  1721.             self.updating = True
  1722.             self.ufeed.signalChange(needsSave=False)
  1723.  
  1724.         if not self.initialHTML is None:
  1725.             html = self.initialHTML
  1726.             self.initialHTML = None
  1727.             redirURL=self.url
  1728.             status = 200
  1729.             charset = self.initialCharset
  1730.             self.initialCharset = None
  1731.             subLinks = self.scrapeLinks(html, redirURL, charset=charset, setTitle = True)
  1732.             self.processLinks(subLinks,0,0)
  1733.             self.checkDone()
  1734.         else:
  1735.             self.getHTML([self.url], top = True)
  1736.  
  1737.     def scrapeLinks(self,html,baseurl,setTitle = False,charset = None):
  1738.         try:
  1739.             if not charset is None:
  1740.                 html = fixHTMLHeader(html,charset)
  1741.             xmldata = html
  1742.             parser = xml.sax.make_parser()
  1743.             parser.setFeature(xml.sax.handler.feature_namespaces, 1)
  1744.             try: parser.setFeature(xml.sax.handler.feature_external_ges, 0)
  1745.             except: pass
  1746.             if charset is not None:
  1747.                 handler = RSSLinkGrabber(baseurl,charset)
  1748.             else:
  1749.                 handler = RSSLinkGrabber(baseurl)
  1750.             parser.setContentHandler(handler)
  1751.             try:
  1752.                 parser.parse(StringIO(xmldata))
  1753.             except IOError, e:
  1754.                 pass
  1755.             except AttributeError:
  1756.                 # bug in the python standard library causes this to be raised
  1757.                 # sometimes.  See #3201.
  1758.                 pass
  1759.             links = handler.links
  1760.             linkDict = {}
  1761.             for link in links:
  1762.                 if link[0].startswith('http://') or link[0].startswith('https://'):
  1763.                     if not linkDict.has_key(toUni(link[0],charset)):
  1764.                         linkDict[toUni(link[0],charset)] = {}
  1765.                     if not link[1] is None:
  1766.                         linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  1767.                     if not link[2] is None:
  1768.                         linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  1769.             if setTitle and not handler.title is None:
  1770.                 self.ufeed.confirmDBThread()
  1771.                 try:
  1772.                     self.title = toUni(handler.title,charset)
  1773.                 finally:
  1774.                     self.ufeed.signalChange()
  1775.             return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')], linkDict)
  1776.         except (xml.sax.SAXException, ValueError, IOError, xml.sax.SAXNotRecognizedException):
  1777.             (links, linkDict) = self.scrapeHTMLLinks(html,baseurl,setTitle=setTitle, charset=charset)
  1778.             return (links, linkDict)
  1779.  
  1780.     ##
  1781.     # Given a string containing an HTML file, return a dictionary of
  1782.     # links to titles and thumbnails
  1783.     def scrapeHTMLLinks(self,html, baseurl,setTitle=False, charset = None):
  1784.         lg = HTMLLinkGrabber()
  1785.         links = lg.getLinks(html, baseurl)
  1786.         if setTitle and not lg.title is None:
  1787.             self.ufeed.confirmDBThread()
  1788.             try:
  1789.                 self.title = toUni(lg.title, charset)
  1790.             finally:
  1791.                 self.ufeed.signalChange()
  1792.             
  1793.         linkDict = {}
  1794.         for link in links:
  1795.             if link[0].startswith('http://') or link[0].startswith('https://'):
  1796.                 if not linkDict.has_key(toUni(link[0],charset)):
  1797.                     linkDict[toUni(link[0],charset)] = {}
  1798.                 if not link[1] is None:
  1799.                     linkDict[toUni(link[0],charset)]['title'] = toUni(link[1],charset).strip()
  1800.                 if not link[2] is None:
  1801.                     linkDict[toUni(link[0],charset)]['thumbnail'] = toUni(link[2],charset)
  1802.         return ([x[0] for x in links if x[0].startswith('http://') or x[0].startswith('https://')],linkDict)
  1803.         
  1804.     ##
  1805.     # Called by pickle during deserialization
  1806.     def onRestore(self):
  1807.         FeedImpl.onRestore(self)
  1808.         #self.itemlist = defaultDatabase.filter(lambda x:isinstance(x,Item) and x.feed is self)
  1809.  
  1810.         #FIXME: the update dies if all of the items aren't restored, so we 
  1811.         # wait a little while before we start the update
  1812.         self.downloads = set()
  1813.         self.tempHistory = {}
  1814.         self.scheduleUpdateEvents(.1)
  1815.  
  1816. class DirectoryWatchFeedImpl(FeedImpl):
  1817.     def __init__(self,ufeed, directory, visible = True):
  1818.         self.dir = directory
  1819.         self.firstUpdate = True
  1820.         if directory is not None:
  1821.             url = u"dtv:directoryfeed:%s" % (makeURLSafe (directory),)
  1822.         else:
  1823.             url = u"dtv:directoryfeed"
  1824.         title = directory
  1825.         if title[-1] == '/':
  1826.             title = title[:-1]
  1827.         title = filenameToUnicode(os.path.basename(title)) + "/"
  1828.         FeedImpl.__init__(self,url = url,ufeed=ufeed,title = title,visible = visible)
  1829.  
  1830.         self.setUpdateFrequency(5)
  1831.         self.scheduleUpdateEvents(0)
  1832.  
  1833.     ##
  1834.     # Directory Items shouldn't automatically expire
  1835.     def expireItems(self):
  1836.         pass
  1837.  
  1838.     def setUpdateFrequency(self, frequency):
  1839.         newFreq = frequency*60
  1840.         if newFreq != self.updateFreq:
  1841.             self.updateFreq = newFreq
  1842.             self.scheduleUpdateEvents(-1)
  1843.  
  1844.     def setVisible(self, visible):
  1845.         if self.visible == visible:
  1846.             return
  1847.         self.visible = visible
  1848.         self.signalChange()
  1849.  
  1850.     def update(self):
  1851.         def isBasenameHidden(filename):
  1852.             if filename[-1] == os.sep:
  1853.                 filename = filename[:-1]
  1854.             return os.path.basename(filename)[0] == FilenameType('.')
  1855.         self.ufeed.confirmDBThread()
  1856.  
  1857.         # Files known about by real feeds (other than other directory
  1858.         # watch feeds)
  1859.         knownFiles = set()
  1860.         for item in views.toplevelItems:
  1861.             if not item.getFeed().getURL().startswith("dtv:directoryfeed"):
  1862.                 knownFiles.add(os.path.normcase(item.getFilename()))
  1863.  
  1864.         # Remove items that are in feeds, but we have in our list
  1865.         for item in self.items:
  1866.             if item.getFilename() in knownFiles:
  1867.                 item.remove()
  1868.  
  1869.         # Now that we've checked for items that need to be removed, we
  1870.         # add our items to knownFiles so that they don't get added
  1871.         # multiple times to this feed.
  1872.         for x in self.items:
  1873.             knownFiles.add(os.path.normcase (x.getFilename()))
  1874.  
  1875.         #Adds any files we don't know about
  1876.         #Files on the filesystem
  1877.         if os.path.isdir(self.dir):
  1878.             all_files = []
  1879.             files, dirs = miro_listdir(self.dir)
  1880.             for file in files:
  1881.                 all_files.append(file)
  1882.             for dir in dirs:
  1883.                 subfiles, subdirs = miro_listdir(dir)
  1884.                 for subfile in subfiles:
  1885.                     all_files.append(subfile)
  1886.             for file in all_files:
  1887.                 if file not in knownFiles and filetypes.isVideoFilename(platformutils.filenameToUnicode(file)):
  1888.                     FileItem(file, feed_id=self.ufeed.id)
  1889.  
  1890.         for item in self.items:
  1891.             if not os.path.isfile(item.getFilename()):
  1892.                 item.remove()
  1893.         if self.firstUpdate:
  1894.             for item in self.items:
  1895.                 item.markItemSeen()
  1896.             self.firstUpdate = False
  1897.  
  1898.         self.scheduleUpdateEvents(-1)
  1899.  
  1900.     def onRestore(self):
  1901.         FeedImpl.onRestore(self)
  1902.         #FIXME: the update dies if all of the items aren't restored, so we 
  1903.         # wait a little while before we start the update
  1904.         self.scheduleUpdateEvents(.1)
  1905.  
  1906. ##
  1907. # A feed of all of the Movies we find in the movie folder that don't
  1908. # belong to a "real" feed.  If the user changes her movies folder, this feed
  1909. # will continue to remember movies in the old folder.
  1910. #
  1911. class DirectoryFeedImpl(FeedImpl):
  1912.     def __init__(self,ufeed):
  1913.         FeedImpl.__init__(self,url = u"dtv:directoryfeed",ufeed=ufeed,title = u"Feedless Videos",visible = False)
  1914.  
  1915.         self.setUpdateFrequency(5)
  1916.         self.scheduleUpdateEvents(0)
  1917.  
  1918.     ##
  1919.     # Directory Items shouldn't automatically expire
  1920.     def expireItems(self):
  1921.         pass
  1922.  
  1923.     def setUpdateFrequency(self, frequency):
  1924.         newFreq = frequency*60
  1925.         if newFreq != self.updateFreq:
  1926.                 self.updateFreq = newFreq
  1927.                 self.scheduleUpdateEvents(-1)
  1928.  
  1929.     def update(self):
  1930.         self.ufeed.confirmDBThread()
  1931.         moviesDir = config.get(prefs.MOVIES_DIRECTORY)
  1932.         # Files known about by real feeds
  1933.         knownFiles = set()
  1934.         for item in views.toplevelItems:
  1935.             if item.feed_id is not self.ufeed.id:
  1936.                 knownFiles.add(os.path.normcase(item.getFilename()))
  1937.             if item.isContainerItem:
  1938.                 item.findNewChildren()
  1939.  
  1940.         knownFiles.add(os.path.normcase(os.path.join(moviesDir, "Incomplete Downloads")))
  1941.  
  1942.         # Remove items that are in feeds, but we have in our list
  1943.         for item in self.items:
  1944.             if item.getFilename() in knownFiles:
  1945.                 item.remove()
  1946.  
  1947.         # Now that we've checked for items that need to be removed, we
  1948.         # add our items to knownFiles so that they don't get added
  1949.         # multiple times to this feed.
  1950.         for x in self.items:
  1951.             knownFiles.add(os.path.normcase (x.getFilename()))
  1952.  
  1953.         #Adds any files we don't know about
  1954.         #Files on the filesystem
  1955.         if os.path.isdir(moviesDir):
  1956.             files, dirs = miro_listdir(moviesDir)
  1957.             for file in files:
  1958.                 if not file in knownFiles:
  1959.                     FileItem(file, feed_id=self.ufeed.id)
  1960.             for dir in dirs:
  1961.                 if dir in knownFiles:
  1962.                     continue
  1963.                 found = 0
  1964.                 not_found = []
  1965.                 subfiles, subdirs = miro_listdir(dir)
  1966.                 for subfile in subfiles:
  1967.                     if subfile in knownFiles:
  1968.                         found = found + 1
  1969.                     else:
  1970.                         not_found.append(subfile)
  1971.                 for subdir in subdirs:
  1972.                     if subdir in knownFiles:
  1973.                         found = found + 1
  1974.                 # If every subfile or subdirectory is
  1975.                 # already in the database (including
  1976.                 # the case where the directory is
  1977.                 # empty) do nothing.
  1978.                 if len(not_found) > 0:
  1979.                     # If there were any files found,
  1980.                     # this is probably a channel
  1981.                     # directory that someone added
  1982.                     # some thing to.  There are few
  1983.                     # other cases where a directory
  1984.                     # would have some things shown.
  1985.                     if found != 0:
  1986.                         for subfile in not_found:
  1987.                             FileItem(subfile, feed_id=self.ufeed.id)
  1988.                     # But if not, it's probably a
  1989.                     # directory added wholesale.
  1990.                     else:
  1991.                         FileItem(dir, feed_id=self.ufeed.id)
  1992.  
  1993.         for item in self.items:
  1994.             if not os.path.exists(item.getFilename()):
  1995.                 item.remove()
  1996.  
  1997.         self.scheduleUpdateEvents(-1)
  1998.  
  1999.     def onRestore(self):
  2000.         FeedImpl.onRestore(self)
  2001.         #FIXME: the update dies if all of the items aren't restored, so we 
  2002.         # wait a little while before we start the update
  2003.         self.scheduleUpdateEvents(.1)
  2004.  
  2005. ##
  2006. # Search and Search Results feeds
  2007.  
  2008. class SearchFeedImpl (RSSFeedImpl):
  2009.     
  2010.     def __init__(self, ufeed):
  2011.         RSSFeedImpl.__init__(self, url=u'', ufeed=ufeed, title=u'dtv:search', visible=False)
  2012.         self.initialUpdate = True
  2013.         self.setUpdateFrequency(-1)
  2014.         self.searching = False
  2015.         self.lastEngine = u'youtube'
  2016.         self.lastQuery = u''
  2017.         self.ufeed.autoDownloadable = False
  2018.         self.ufeed.signalChange()
  2019.  
  2020.     @returnsUnicode
  2021.     def quoteLastQuery(self):
  2022.         return escape(self.lastQuery)
  2023.  
  2024.     @returnsUnicode
  2025.     def getURL(self):
  2026.         return u'dtv:search'
  2027.  
  2028.     @returnsUnicode
  2029.     def getTitle(self):
  2030.         return _(u'Search')
  2031.  
  2032.     @returnsUnicode
  2033.     def getStatus(self):
  2034.         status = u'idle-empty'
  2035.         if self.searching:
  2036.             status =  u'searching'
  2037.         elif len(self.items) > 0:
  2038.             status =  u'idle-with-results'
  2039.         elif self.url:
  2040.             status = u'idle-no-results'
  2041.         return status
  2042.  
  2043.     def reset(self, url=u'', searchState=False):
  2044.         self.ufeed.confirmDBThread()
  2045.         try:
  2046.             for item in self.items:
  2047.                 item.remove()
  2048.             self.url = url
  2049.             self.searching = searchState
  2050.             self.thumbURL = defaultFeedIconURL()
  2051.             self.ufeed.iconCache.remove()
  2052.             self.ufeed.iconCache = IconCache(self.ufeed, is_vital = True)
  2053.             self.ufeed.iconCache.requestUpdate(True)
  2054.             self.initialHTML = None
  2055.             self.etag = None
  2056.             self.modified = None
  2057.             self.parsed = None
  2058.         finally:
  2059.             self.ufeed.signalChange()
  2060.     
  2061.     def preserveDownloads(self, downloadsFeed):
  2062.         self.ufeed.confirmDBThread()
  2063.         for item in self.items:
  2064.             if item.getState() not in ('new', 'not-downloaded'):
  2065.                 item.setFeed(downloadsFeed.id)
  2066.         
  2067.     def lookup(self, engine, query):
  2068.         checkU(engine)
  2069.         checkU(query)
  2070.         url = searchengines.getRequestURL(engine, query)
  2071.         self.reset(url, True)
  2072.         self.lastQuery = query
  2073.         self.lastEngine = engine
  2074.         self.update()
  2075.         self.ufeed.signalChange()
  2076.  
  2077.     def _handleNewEntry(self, entry):
  2078.         """Handle getting a new entry from a feed."""
  2079.         videoEnc = getFirstVideoEnclosure(entry)
  2080.         if videoEnc is not None:
  2081.             url = videoEnc.get('url')
  2082.             if url is not None:
  2083.                 dl = downloader.getExistingDownloaderByURL(url)
  2084.                 if dl is not None:
  2085.                     for item in dl.itemList:
  2086.                         if item.getFeedURL() == 'dtv:searchDownloads' and item.getURL() == url:
  2087.                             try:
  2088.                                 if entry["id"] == item.getRSSID():
  2089.                                     item.setFeed(self.ufeed.id)
  2090.                                     if not _entry_equal(entry, item.getRSSEntry()):
  2091.                                         self._handleNewEntryForItem(item, entry)
  2092.                                     return
  2093.                             except KeyError:
  2094.                                 pass
  2095.                             title = entry.get("title")
  2096.                             oldtitle = item.entry.get("title")
  2097.                             if title == oldtitle:
  2098.                                 item.setFeed(self.ufeed.id)
  2099.                                 if not _entry_equal(entry, item.getRSSEntry()):
  2100.                                     self._handleNewEntryForItem(item, entry)
  2101.                                 return
  2102.         RSSFeedImpl._handleNewEntry(self, entry)
  2103.  
  2104.     def updateUsingParsed(self, parsed):
  2105.         self.searching = False
  2106.         RSSFeedImpl.updateUsingParsed(self, parsed)
  2107.  
  2108.     def update(self):
  2109.         if self.url is not None and self.url != u'':
  2110.             RSSFeedImpl.update(self)
  2111.  
  2112.     def feedparser_errback(self, e):
  2113.         if self.searching:
  2114.             self.searching = False
  2115.         RSSFeedImpl.feedparser_errback(self, e)
  2116.  
  2117.     def _updateErrback(self, error):
  2118.         if self.searching:
  2119.             self.searching = False
  2120.         RSSFeedImpl._updateErrback(self, error)
  2121.  
  2122. class SearchDownloadsFeedImpl(FeedImpl):
  2123.     def __init__(self, ufeed):
  2124.         FeedImpl.__init__(self, url=u'dtv:searchDownloads', ufeed=ufeed, 
  2125.                 title=None, visible=False)
  2126.         self.setUpdateFrequency(-1)
  2127.  
  2128.     @returnsUnicode
  2129.     def getTitle(self):
  2130.         return _(u'Search')
  2131.  
  2132. class ManualFeedImpl(FeedImpl):
  2133.     """Downloaded Videos/Torrents that have been added using by the
  2134.     user opening them with democracy.
  2135.     """
  2136.     def __init__(self, ufeed):
  2137.         FeedImpl.__init__(self, url=u'dtv:manualFeed', ufeed=ufeed, 
  2138.                 title=None, visible=False)
  2139.         self.ufeed.expire = u'never'
  2140.         self.setUpdateFrequency(-1)
  2141.  
  2142.     @returnsUnicode
  2143.     def getTitle(self):
  2144.         return _(u'Local Files')
  2145.  
  2146. class SingleFeedImpl(FeedImpl):
  2147.     """Single Video that is playing that has been added by the user
  2148.     opening them with democracy.
  2149.     """
  2150.     def __init__(self, ufeed):
  2151.         FeedImpl.__init__(self, url=u'dtv:singleFeed', ufeed=ufeed, 
  2152.                 title=None, visible=False)
  2153.         self.ufeed.expire = u'never'
  2154.         self.setUpdateFrequency(-1)
  2155.  
  2156.     @returnsUnicode
  2157.     def getTitle(self):
  2158.         return _(u'Playing File')
  2159.  
  2160. ##
  2161. # Parse HTML document and grab all of the links and their title
  2162. # FIXME: Grab link title from ALT tags in images
  2163. # FIXME: Grab document title from TITLE tags
  2164. class HTMLLinkGrabber(HTMLParser):
  2165.     linkPattern = re.compile("<(a|embed)\s[^>]*(href|src)\s*=\s*\"([^\"]*)\"[^>]*>(.*?)</a(.*)", re.S)
  2166.     imgPattern = re.compile(".*<img\s.*?src\s*=\s*\"(.*?)\".*?>", re.S)
  2167.     tagPattern = re.compile("<.*?>")
  2168.     def getLinks(self,data, baseurl):
  2169.         self.links = []
  2170.         self.lastLink = None
  2171.         self.inLink = False
  2172.         self.inObject = False
  2173.         self.baseurl = baseurl
  2174.         self.inTitle = False
  2175.         self.title = None
  2176.         self.thumbnailUrl = None
  2177.  
  2178.         match = HTMLLinkGrabber.linkPattern.search(data)
  2179.         while match:
  2180.             try:
  2181.                 linkURL = match.group(3).encode('ascii')
  2182.             except UnicodeError:
  2183.                 linkURL = match.group(3)
  2184.                 i = len (linkURL) - 1
  2185.                 while (i >= 0):
  2186.                     if 127 < ord(linkURL[i]) <= 255:
  2187.                         linkURL = linkURL[:i] + "%%%02x" % (ord(linkURL[i])) + linkURL[i+1:]
  2188.                     i = i - 1
  2189.  
  2190.             link = urljoin(baseurl, linkURL)
  2191.             desc = match.group(4)
  2192.             imgMatch = HTMLLinkGrabber.imgPattern.match(desc)
  2193.             if imgMatch:
  2194.                 try:
  2195.                     thumb = urljoin(baseurl, imgMatch.group(1).encode('ascii'))
  2196.                 except UnicodeError:
  2197.                     thumb = None
  2198.             else:
  2199.                 thumb = None
  2200.             desc =  HTMLLinkGrabber.tagPattern.sub(' ',desc)
  2201.             self.links.append((link, desc, thumb))
  2202.             match = HTMLLinkGrabber.linkPattern.search(match.group(5))
  2203.         return self.links
  2204.  
  2205. class RSSLinkGrabber(xml.sax.handler.ContentHandler, xml.sax.handler.ErrorHandler):
  2206.     def __init__(self,baseurl,charset=None):
  2207.         self.baseurl = baseurl
  2208.         self.charset = charset
  2209.     def startDocument(self):
  2210.         #print "Got start document"
  2211.         self.enclosureCount = 0
  2212.         self.itemCount = 0
  2213.         self.links = []
  2214.         self.inLink = False
  2215.         self.inDescription = False
  2216.         self.inTitle = False
  2217.         self.inItem = False
  2218.         self.descHTML = ''
  2219.         self.theLink = ''
  2220.         self.title = None
  2221.         self.firstTag = True
  2222.         self.errors = 0
  2223.         self.fatalErrors = 0
  2224.  
  2225.     def startElementNS(self, name, qname, attrs):
  2226.         uri = name[0]
  2227.         tag = name[1]
  2228.         if self.firstTag:
  2229.             self.firstTag = False
  2230.             if tag not in ['rss','feed']:
  2231.                 raise xml.sax.SAXNotRecognizedException, "Not an RSS file"
  2232.         if tag.lower() == 'enclosure' or tag.lower() == 'content':
  2233.             self.enclosureCount += 1
  2234.         elif tag.lower() == 'link':
  2235.             self.inLink = True
  2236.             self.theLink = ''
  2237.         elif tag.lower() == 'description':
  2238.             self.inDescription = True
  2239.             self.descHTML = ''
  2240.         elif tag.lower() == 'item':
  2241.             self.itemCount += 1
  2242.             self.inItem = True
  2243.         elif tag.lower() == 'title' and not self.inItem:
  2244.             self.inTitle = True
  2245.  
  2246.     def endElementNS(self, name, qname):
  2247.         uri = name[0]
  2248.         tag = name[1]
  2249.         if tag.lower() == 'description':
  2250.             lg = HTMLLinkGrabber()
  2251.             try:
  2252.                 html = xhtmlify(unescape(self.descHTML),addTopTags=True)
  2253.                 if not self.charset is None:
  2254.                     html = fixHTMLHeader(html,self.charset)
  2255.                 self.links[:0] = lg.getLinks(html,self.baseurl)
  2256.             except HTMLParseError: # Don't bother with bad HTML
  2257.                 logging.info ("bad HTML in description for %s", self.baseurl)
  2258.             self.inDescription = False
  2259.         elif tag.lower() == 'link':
  2260.             self.links.append((self.theLink,None,None))
  2261.             self.inLink = False
  2262.         elif tag.lower() == 'item':
  2263.             self.inItem == False
  2264.         elif tag.lower() == 'title' and not self.inItem:
  2265.             self.inTitle = False
  2266.  
  2267.     def characters(self, data):
  2268.         if self.inDescription:
  2269.             self.descHTML += data
  2270.         elif self.inLink:
  2271.             self.theLink += data
  2272.         elif self.inTitle:
  2273.             if self.title is None:
  2274.                 self.title = data
  2275.             else:
  2276.                 self.title += data
  2277.  
  2278.     def error(self, exception):
  2279.         self.errors += 1
  2280.  
  2281.     def fatalError(self, exception):
  2282.         self.fatalErrors += 1
  2283.  
  2284. # Grabs the feed link from the given webpage
  2285. class HTMLFeedURLParser(HTMLParser):
  2286.     def getLink(self,baseurl,data):
  2287.         self.baseurl = baseurl
  2288.         self.link = None
  2289.         try:
  2290.             self.feed(data)
  2291.         except HTMLParseError:
  2292.             logging.info ("error parsing %s", baseurl)
  2293.         try:
  2294.             self.close()
  2295.         except HTMLParseError:
  2296.             logging.info ("error closing %s", baseurl)
  2297.         return self.link
  2298.  
  2299.     def handle_starttag(self, tag, attrs):
  2300.         attrdict = {}
  2301.         for (key, value) in attrs:
  2302.             attrdict[key.lower()] = value
  2303.         if (tag.lower() == 'link' and attrdict.has_key('rel') and 
  2304.             attrdict.has_key('type') and attrdict.has_key('href') and
  2305.             attrdict['rel'].lower() == 'alternate' and 
  2306.             attrdict['type'].lower() in ['application/rss+xml',
  2307.                                          'application/podcast+xml',
  2308.                                          'application/rdf+xml',
  2309.                                          'application/atom+xml',
  2310.                                          'text/xml',
  2311.                                          'application/xml']):
  2312.             self.link = urljoin(self.baseurl,attrdict['href'])
  2313.  
  2314. def expireItems():
  2315.     try:
  2316.         for feed in views.feeds:
  2317.             feed.expireItems()
  2318.     finally:
  2319.         eventloop.addTimeout(300, expireItems, "Expire Items")
  2320.  
  2321. def getFeedByURL(url):
  2322.     return views.feeds.getItemWithIndex(indexes.feedsByURL, url)
  2323.